Pinvon's Blog

所见, 所闻, 所思, 所想

C++ 快速学习

函数与参数

传值参数

int abc(int a, int b, int c) {
    return a+b+b*c+(a+b-c)/(a+b)+4;
}

在运行时, 实际值会在函数执行前被复制给形参, 复制过程由形参对应的数据类型的 拷贝构造函数 来完成, 当函数结束时, 形参所属数据类型的 析构函数 负责释放该形参. 所以当一个函数返回时, 形参的值不会被复制到对应的实参中, 所以, 传值情况下, 函数调用不会修改实际参数的值.

模板函数

将上面的函数改成用模板来实现, 可以在多种数据类型之间通用, 将参数的数据类型作为一个变量, 它的值由编译器来确定. 如:

template<typename T>
T abc(T a, T b, T c) {
    return a+b+b*c+(a+b-c)/(a+b)+4;
}

// ...

// 调用
int x=2, y=4;
abc(2, x, y);

引用参数

使用传值参数, 会在一定程度上降低程序的效率. 假设数据类型是用户自定义的 Matrix 类, 其拷贝构造函数将复制所有元素, 析构函数将释放所有元素. 如果我们用具有 1000 个元素的 Matrix 作为实际参数来调用 abc(), 则复制给 3 个参数需要 3000 次操作, 析构时又要进行 3000 次操作.

如果使用引用参数, 在函数被调用时, 不会复制实参的值, 函数调用会修改实参的值.

可以将上面的模板函数改成:

template<typename T>
T abc(T& a, T& b, T& c) {
    return a+b+b*c+(a+b-c)/(a+b)+4;
}

// ...
// 调用
int x = 1, y = 2;
cout << abc(x, x, y) << endl;

常量引用参数

使用常量引用参数, 可以使得函数不能修改引用参数的值. 这在软件工程方面具有重要的意义, 用户可以立即了解到该函数并不会修改实际参数.

template<typename T>
T abc(const T& a, const T& b, const T& c) {
    return a+b+b*c+(a+b-c)/(a+b)+4;
}

如果想让程序更加通用, 可以使用三种类型, 如: template<typename T1, typename T2, typename T3>.

返回值

在以上的函数中, 返回值就是一个具体值, 这意味着返回的对象会被复制到调用环境中. 只要在释放临时变量以及形参的空间之前, 将该值返回, 就不会丢失这个值.

如果需要返回引用, 可以这么写:

template<typename T>
T& func(int i, T& z) {
    // ...
    return z;
}

当函数返回时, 传值形参 i 及其他局部变量将被释放, 而 z 是对实际参数的引用, 不会受到影响.

如果这么写, 则返回值会丢失:

int& func(int i, int& z) {
    // ...
    int* ip = &i;
    return ip;  // 丢失
}

如果要返回常量引用, 可以这么写:

template<typename T>
const T& func(int i, T& z) {
    // ...
}

递归函数

递归函数就是自己调用自己, 有直接递归和间接递归两种.

直接递归: 函数 F 中直接包含了函数 F.

间接递归: 函数 F 中包含函数 G, 函数 G 又调用函数 H, ..., 最后一个函数又调用了函数 F.

C++ 允许我们编写递归函数, 递归函数必须包含终止条件. 如计算阶乘:

int Factorial(int n) {
    if (n <= 1) return 1;  // 终止条件
    return n*Factorial(n-1);
}

动态存储分配

new

new 操作符会返回一个指向所分配空间的指针. 如果要给一个整数动态分配存储空间, 并在刚分配的空间中存储 10 这个整数, 可以这么写:

int *y = new int;
*y = 10;

// 或者
int *y = new int(10);

一维数组

一个大小为 n 的一维浮点数组可以这样创建:

float *x = new float[n];

// 返回第 2 个元素
x[1];

异常处理

try catch 机制可以捕获异常. 我们将可能会出现异常的代码放在 try{...} 中, 对异常的处理放在 catch(){...} 中. catch() 可以有多个, 针对不同的异常情况, 进行不同的处理, 如果想直接捕获所有异常, 则这么写: catch(...){}.

在 new 操作时, 如果内存不够分配了, 将会出现异常, 抛出的异常类型为 xalloc, 所以上面的动态内存分配, 可以这么写:

try {
    float *x = new float[10];
} catch (xalloc) {
    cerr << "out of memory" << endl;
    exit(1);
}

如果没有引发异常, 执行完 try 块后, 直接跳过 catch 块.

delete

与 new 相对应的释放操作. 释放方法如下:

delete y;
delete [] x;

二维数组

二维数组在声明时必须确定第二维的大小, 第一维可以动态确定. 如: a[]1 是合法的, 而 a[][] 则不是.

char (*c)[5];
try {
    c = new char [n][5];
} catch (xalloc) {
    cerr << "out of memory" << endl;
    exit(1);
}
template<typename T>
bool Make2DArray (T ** &x, int rows, int cols) {  // 创建一个二维数组
    try{
        x = new T * [rows];  // 创建行指针
        for (int i=0; i<rows; i++) {
            x[i] = new int [cols];
            return true;
        }
    } catch (xalloc) {
        return false;
    }
}

template<typename T>
vod Delete2DArray (T ** &x, int rows) {
    for (int i=0; i<rows; i++) {
        delete [] x[i];
        delete [] x;
        x = 0;
    }
}

货币类 Currency 的声明

使用三个变量来描述一个货币: 符号(+-), 美元(dollars), 美分(cents).

#ifndef CURRENCY_H
#define CURRENCY_H

namespace currency{

    enum sign {
        plus,
        minus
    };

    class Currency{
    public:
        Currency(sign s=plus, unsigned long d=0, unsigned int c=0);  // 构造函数
        ~Currency(){}  // 析构函数
        bool Set(sign s, unsigned long d, unsigned int c);
        bool Set(float a);
        sign Sign() const { return sgn; }
        unsigned long Dollars() const { return dollars; }
        unsigned int Cents() const { return cents; }
        Currency Add(const Currency& x) const;
        Currency& Increment(const Currency& x);
        void Output() const;
    private:
        sign sgn;
        unsigned long dollars;
        unsigned int cents;
    };
}

#endif

这边使用了名字空间, 是因为 plus 和 minus 在标准库中也用到了, 直接使用会出现冲突.

构造函数

构造函数与类名同名, 它指定了如何创建一个给定类型的对象, 不可以有返回值. 在创建一个 Currency 类对象时, 构造函数被自动调用.

创建 Currency 类对象的方式:

Currency f, g(plus, 3, 45), h(minus, 10);
Currency *m = new Currency(plus, 8, 12);

析构函数

析构函数比类名多一个符号 ~. 当 Currency 对象超出作用域时, 自动调用析构函数, 用来删除对象. 在这个例子中, 析构函数是空的, 但是如果有些类创建了动态数组, 则需要在析构函数中释放这些空间. 析构函数也没有返回值.

拷贝构造函数

Add 函数和 Increment 函数都会返回 Currency 类对象, 但 Add 函数返回的是值, Increment 函数返回的是引用.

拷贝构造函数用来执行返回值的复制及传值参数的复制. 在例子中, 我们没有给出拷贝构造函数, 所以 C++ 会使用默认的拷贝构造函数, 默认的拷贝构造函数进行数据成员的复制, 如果需要做其他工作, 则需要自己实现.

货币类 Currency 的实现

#include"currency.h"
#include<iostream>
#include<cstdlib>

using namespace currency;

//构造函数
Currency::Currency(sign s, unsigned long d, unsigned int c) {
    if (c > 99) {
        std::cerr << "Cents should be < 100" << std::endl;
        exit(1);
    }
    sgn = s;
    dollars = d;
    cents = c;
}

// 设置 private 数据成员
bool Currency::Set(sign s, unsigned long d, unsigned int c) {
    if (c > 99) return false;
    sgn = s;
    dollars = d;
    cents = c;
    return true;
}

bool Currency::Set(float a) {
    if (a < 0) {
        sgn = minus;
        a = -a;
    } else {
        sgn = plus;
    }

    dollars = a;  // 抽取整数部分
    // 形如 a.bc 这种格式的数字, 在计算机中可能没有一个精确的表示. 例如, 计算机所描述的数字 5.29, 可能
    // 比 5.29 稍微小一点. 所以对 (a-dollars)*100 得到的数进行取整, 得到的会是 28, 而不是 29. 由于
    // 我们只要取两位数, 所以将其加上 0.005. 如果要取三位数, 则加上 0.0005.
    cents = (a + 0.005 - dollars) * 100;
    return true;
}

// 累加两个 Currency
// 计算机进行小数运算时, 会丢失精度, 所以将两个 Currency 进行累加时, 先将其转化成整数, 再进行计算
Currency Currency::Add(const Currency& x) const {
    long a1, a2, a3;
    Currency ans;
    a1 = dollars * 100 + cents;
    if (sgn == minus) a1 = -a1;

    a2 = x.dollars * 100 + x.cents;
    if (x.sgn == minus) a2 = -a2;

    a3 = a1 + a2;

    if (a3 < 0) {
        ans.sgn = minus;
        a3 = -a3;
    } else {
        ans.sgn = plus;
    }
    ans.dollars = a3 / 100;
    ans.cents = a3 - ans.dollars * 100;
    return ans;
}

Currency& Currency::Increment(const Currency& x) {
    // this 是指向当前对象的指针, 所以 *this 表示当前对象
    *this = Add(x);
    return *this;
}

void Currency::Output() const {
    if (sgn == minus) {
        std::cout << '-';
    }
    std::cout << '$' << dollars << '.';

    if (cents < 10) {
        std::cout << "0";
    }
    std::cout << cents;
}

由于 Currency 类的数据成员都是私有的, 所以用户不能通过如下语句来改变这些数据成员的值:

h.cents = 20;
h.dollars = 100;
h.sgn = plus;

而是只能通过构造函数和 Set 函数来进行设置, 这两个函数会判断数据成员是否合法, 而 Add 等函数则不再验证.

货币类 Currency 的使用

#include <iostream>
#include "currency.h"
using namespace currency;

int main(int argc, char const *argv[])
{
    /* code */
    Currency g, h(plus, 3, 50), i, j;
    g.Set(minus, 2, 25);
    i.Set(-6.45);
    j = h.Add(g);
    j.Output();
    std::cout << std::endl;
    j = i.Add(g).Add(h);
    j.Output();
    std::cout << std::endl;
    j = i.Increment(g).Add(h);
    j.Output();
    std::cout << std::endl;
    i.Output();
    std::cout << std::endl;
    return 0;
}

Makefile

main: main.o currency.o
    g++ main.o currency.o -o main
main.o: main.cpp
    g++ -c main.cpp -o main.o
currency.o: currency.cpp
    g++ -c currency.cpp -o currency.o
clean:
    rm *.o
    rm main

运算符重载

在 Currency 类中, Add 函数和 Increment 函数相当于平时用到的 + 和 += 操作符, 直接使用这两个操作符, 会更加自然. 另外, Output 函数相当于 << 操作符.

操作符重载允许我们对 C++ 操作符进行扩展, 使其能应用到新的数据类型或类.

为简单起见, 从现在开始, 我们的 Currency 类只有一个私有数据成员: long amount, $1.32 直接用数字 132 来表示.

Currency operator+(const Currency& x) const {
    Currency y;
    y.amount = amount + x.amount;
    return y;
}

Currency& operator+=(const Currency& x) {
    amount += x.amount;
    return *this;
}

void Output(ostream& out) const {
    long a = amount;
    if (a < 0) {
        out << '-';
        a = -a;
    }
    long d = a / 100;
    out << '$' << d << '.';
    int c = a - d * 100;
    if (c < 10) out << "0";
    out << c;
}

ostream& operator<<(ostream& out, const Currency& x) {
    x.Output(output);
    return out;
}

// 使用
cout << i << endl;  // i 是 Currency 对象

引发异常

像构造函数和 Set 函数, 有可能会在执行预定的任务时失败. 可以定义一个异常类, 在引发异常时, 捕获异常并处理.

class BadInitializers {  // 定义异常类
public:
    BadInitializers() {}
};
Currency::Currency(sign s, unsigned long d, unsigned int c) {
    if (c > 99) throw BadInitializers();
    // ...
}

void Currency::Set(sign s, unsigned long d, unsigned int c) {
    if (c > 99) throw BadInitializers();
    // ...
}

友元函数

一个类的 private 成员仅对于本类的成员函数是可见的. 但是有时候, 必须把对这些 private 成员的访问权限授予其他的类和函数, 于是就有了友元函数.

像前面的 << 运算符重载中, Output 函数是 Currency 类的成员函数, operator<< 则不是, 所以编写时会比较麻烦, 要在 operator<< 函数内部调用 Output 函数.

有了友元函数, 我们可以直接使用 operator<<, 而不再需要 Output 函数.

class Currency{
public:
    friend ostream& operator<<(ostream&, const Currency&);
};

ostream& operator<<(ostream& out, const Currency& x) {
    long a = x.amount;
    if (a < 0) { out << '-'; a = -a; }
    long d = a / 100;
    out << '$' << d << '.';
    int c = a - d * 100;
    if (c < 10) out << "0";
    out << c;
    return out;
}

为什么不能直接将 operator<< 设置为成员函数?

因为流操作符的左边必须是成员函数所属的类, 但是实际上, 流操作符都是 cin, cout, cerr 等, 这不是我们所能修改的, 所以最好的办法是将其定义为友元.

protected

#ifndef, #define, #endif 语句

建议为每个头文件都加上这些语句, 这样可以确保这些头文件仅被包含和编译一次.

测试

黑盒测试

假设程序不可见, 仅设计输入和期望的输出.

白盒测试

程序可见, 设计测试数据, 使得程序的每一条语句都至少被执行一次(如 if 分支, 都要跑一遍).

Footnotes:

1

DEFINITION NOT FOUND.

Comments

使用 Disqus 评论
comments powered by Disqus